package org.vaadin.dotmap.client.ui;

import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.LoadHandler;
import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.Element;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Image;
import com.vaadin.terminal.gwt.client.ApplicationConnection;
import com.vaadin.terminal.gwt.client.BrowserInfo;
import com.vaadin.terminal.gwt.client.Paintable;
import com.vaadin.terminal.gwt.client.UIDL;
import com.vaadin.terminal.gwt.client.Util;

public class VDotMap extends AbsolutePanel implements Paintable {

    public static final int ZINDEX_UNDER = 0;
    public static final int ZINDEX_MAP = 2;
    public static final int ZINDEX_OVER = 3;

    public static final String ATTR_MAP_IMAGE = "img";
    public static final String ATTR_MAP_COLOR = "bg";
    public static final String ATTR_MAP_TOP_CROP = "tc";
    public static final String ATTR_HIGHLIGHT_IMAGE = "hl";
    public static final String ATTR_HIGHLIGHT_ZINDEX = "z";
    public static final String ATTR_FADE_MSEC = "fm";
    public static final String TAG_HIGHLIGHT = "hl";

    public static final String CLASSNAME = "v-dotmap";

    protected String width = null;
    protected String height = null;

    protected int topCrop = 0;

    protected static int FADE_ANIMATION_INTERVAL = 50; // == 20 fps
    protected int fadeMsec = 5000;

    protected Image mapImg;
    protected String mapUrl;

    protected int mapWidth = -1;
    protected int mapHeight = -1;

    protected String highlightImage;

    protected List<Highlight> highlightQueue;

    public VDotMap() {
        setStyleName(CLASSNAME);

        mapImg = new Image();
        mapImg.addLoadHandler(new LoadHandler() {
            public void onLoad(LoadEvent event) {
                int nw = mapImg.getWidth();
                int nh = mapImg.getHeight();
                boolean changed = (nw != mapWidth && width == null)
                        || (nh != mapHeight && height == null);
                mapWidth = nw;
                mapHeight = nh;

                if (changed) {
                    if (width == null) {
                        setWidth(mapWidth + "px");
                    }
                    if (height == null) {
                        setHeight(mapHeight + "px");
                    }
                    Util.notifyParentOfSizeChange(VDotMap.this, true);
                }
                showHighlights();
            }
        });
        add(mapImg, 0, 0);
        DOM.setIntStyleAttribute(mapImg.getElement(), "zIndex", ZINDEX_MAP);

    }

    // we're going to guess that the new highlight is the same size as the last,
    // and adjust in the timer
    private int lastHH = 0; // half height
    private int lastHW = 0; // half width

    private void showHighlights() {
        if (this.highlightQueue != null) {
            for (final Highlight h : this.highlightQueue) {
                final int[] px = translate(h.latitude, h.longitude);
                final Image img = new Image();
                img.setUrl((String) h.getImg());
                add(img, px[0] - lastHH, px[1] - lastHW);
                setWidgetPosition(img, px[0] - lastHH, px[1] - lastHW);
                if (h.getZIndex() != ZINDEX_UNDER) {
                    DOM.setIntStyleAttribute(img.getElement(), "zIndex",
                            h.getZIndex());
                }
                Timer fader = new Timer() {
                    private final long start = new Date().getTime();

                    @Override
                    public void run() {
                        // adjust position
                        lastHH = (int) Math.round(0.5 * img.getHeight());
                        lastHW = (int) Math.round(0.5 * img.getWidth());
                        setWidgetPosition(img, px[0] - lastHH, px[1] - lastHW);

                        // fade
                        long now = new Date().getTime();
                        long timeElapsed = now - start;
                        float remainingFraction = 1 - timeElapsed
                                / (float) h.getFadeMsec();
                        int opacity = (int) (100 * remainingFraction);
                        if (opacity <= 0) {
                            cancel();
                            remove(img);
                        } else {
                            setOpacity(img.getElement(), opacity);
                        }
                    }
                };
                fader.scheduleRepeating(FADE_ANIMATION_INTERVAL);
            }
            this.highlightQueue = null;
        }
    }

    private static void setOpacity(Element el, int opacity) {
        if (BrowserInfo.get().isIE()) {
            DOM.setStyleAttribute(el, "filter", "Alpha(opacity=" + opacity
                    + ")");
        } else {
            DOM.setStyleAttribute(el, "opacity", "" + (opacity / 100.0));
            DOM.setStyleAttribute(el, "-moz-opacity", "" + (opacity / 100.0));
            DOM.setStyleAttribute(el, "-webkit-opacity", "" + (opacity / 100.0));
        }
    }

    private int[] translate(double lat, double lon) {
        // First calculate 0..1x0..1 "normalized" mercator (thanks Mapki)
        if (lon > 180) {
            lon -= 360;
        }
        lon /= 360;
        lon += 0.5;
        lat = 0.5 - ((Math.log(Math.tan((Math.PI / 4)
                + (Math.toRadians(0.5 * lat)))) / Math.PI) / 2.0);

        // Let map width dictate
        lat = lat * mapWidth; // should be height
        lon = lon * mapWidth;

        int[] res = { (int) Math.round(lon), (int) Math.round(lat) - topCrop };
        return res;
    }

    public void addHighligt(Highlight h) {
        this.highlightQueue.add(h);
    }

    /**
     * Called whenever an update is received from the server
     */
    public void updateFromUIDL(UIDL uidl, ApplicationConnection client) {

        if (client.updateComponent(this, uidl, true)) {
            return;
        }

        if (uidl.hasAttribute("width")) {
            this.width = uidl.getStringAttribute("width");
            setWidth(width);
        }

        if (uidl.hasAttribute("height")) {
            this.height = uidl.getStringAttribute("height");
            setHeight(height);
        }

        if (uidl.hasAttribute(ATTR_FADE_MSEC)) {
            this.fadeMsec = uidl.getIntAttribute(ATTR_FADE_MSEC);
        }

        if (uidl.hasAttribute(ATTR_MAP_TOP_CROP)) {
            this.topCrop = uidl.getIntAttribute(ATTR_MAP_TOP_CROP);
        }

        if (uidl.hasAttribute(ATTR_HIGHLIGHT_IMAGE)) {
            String u = client.translateVaadinUri(uidl
                    .getStringAttribute(ATTR_HIGHLIGHT_IMAGE));
            this.highlightImage = u;
        }

        if (uidl.hasAttribute(ATTR_MAP_COLOR)) {
            String bg = client.translateVaadinUri(uidl
                    .getStringAttribute(ATTR_MAP_COLOR));
            getElement().getStyle().setBackgroundColor(bg);
        }

        for (Iterator it = uidl.getChildIterator(); it.hasNext();) {
            UIDL s = (UIDL) it.next();
            if (TAG_HIGHLIGHT.equals(s.getTag())) {
                if (this.highlightQueue == null) {
                    this.highlightQueue = new LinkedList<Highlight>();
                }
                final double lat = s.getDoubleAttribute("lat");
                final double lon = s.getDoubleAttribute("lon");
                String img = highlightImage;
                if (s.hasAttribute(ATTR_HIGHLIGHT_IMAGE)) {
                    img = client.translateVaadinUri(s
                            .getStringAttribute(ATTR_HIGHLIGHT_IMAGE));
                }
                long fade = this.fadeMsec;
                if (s.hasAttribute(ATTR_FADE_MSEC)) {
                    fade = s.getLongAttribute(ATTR_FADE_MSEC);
                }
                int z = 0;
                if (s.hasAttribute(ATTR_HIGHLIGHT_ZINDEX)) {
                    z = s.getIntAttribute(ATTR_HIGHLIGHT_ZINDEX);
                }
                addHighligt(new Highlight(lat, lon, img, fade, z));
            }
        }

        String url = uidl.getStringAttribute(ATTR_MAP_IMAGE);
        if (url != null && !url.equals(mapUrl)) {
            mapUrl = url;
            mapImg.setUrl(client.translateVaadinUri(url));
        } else {
            if (width == null) {
                setWidth(mapWidth + "px");
            }
            if (height == null) {
                setHeight(mapHeight + "px");
            }
            Util.notifyParentOfSizeChange(VDotMap.this, true);
            showHighlights();
        }

    }

    public static class Highlight {
        private double latitude;
        private double longitude;
        private Object img = null;
        private long fadeMsec = -1;
        private int zindex = 0;

        public Highlight(double latitude, double longitude) {
            this.latitude = latitude;
            this.longitude = longitude;
        }

        /**
         * Represents one highlight on the map. Used both on client and server
         * for simplicity, which introduces a gotcha: the image is represented
         * as a Resource on the server (Vaadin), a String on the client (GWT).
         * 
         * @param latitude
         * @param longitude
         * @param imageResource
         *            highlight image, Resource on the server, String on the
         *            client
         * @param fadeMsec
         * @param zindex
         */
        public Highlight(double latitude, double longitude,
                Object imageResource, long fadeMsec, int zindex) {
            this.latitude = latitude;
            this.longitude = longitude;
            this.img = imageResource;
            this.fadeMsec = fadeMsec;
            this.zindex = zindex;
        }

        public double getLatitude() {
            return latitude;
        }

        public double getLongitude() {
            return longitude;
        }

        public Object getImg() {
            return img;
        }

        public void setImg(Object img) {
            this.img = img;
        }

        public long getFadeMsec() {
            return fadeMsec;
        }

        public void setFadeMsec(long fadeMsec) {
            this.fadeMsec = fadeMsec;
        }

        public int getZIndex() {
            return zindex;
        }

        public void setZIndex(int zindex) {
            this.zindex = zindex;
        }

    }

}
